/*
 * Logging and control of heating-system
 *
 * Feb-May 2011
 * Author Christoffer Vaaben Ladefoged
 *
 * Note: pumpState HIGH means that the pump in NOT RUNNING
 */
#include <Time.h>
#include <LiquidCrystal.h>

// states
#define INIT 0
#define RUNNING 1
#define CONFIG 2
//#define DATA 3
#define STOPPED 4

#define LOG_SIZE 100
#define backlite 13

#define backliteDuration 20000
#define pumpPrimeDuration 180000
#define autoRestartDuration 3600000
#define logInterval 5000
#define ignoreTime 1000
#define deltaLimit 5
#define forceTemp 45

// Variables will change:
long pumpMillis = 0;
//long measureMillis = 0;
long logMillis = 0;
long backliteMillis = 0;
long debouncMillis = 0;
long updateTime = 0;
long ignoreInputMillis = 0;
long autoRestartMillis = 0;

double pumpQ = 0.5; // kg/sec
double heatCapacity = 4.18; //kJ/kg*K
double dissipatedHeat = 0;
double dissipated = 0;
//int pointer = 0;
//boolean logState[LOG_SIZE];
//byte logT0[LOG_SIZE];
//byte logT1[LOG_SIZE];
//double runningT0avg = 0;
//double runningT1avg = 0;
//boolean looped = false;

boolean displayOn = false;
int pumpState;
int lastReading = LOW;
long bounce;

int t0 = 0;
int t1 = 0;

byte STATE = RUNNING;
byte LAST_STATE = STATE;

// allocation of log-memory
//PROGMEM prog_uchar logT0[1440];
//PROGMEM prog_uchar logT1[1440];
//int logIndex=0;

LiquidCrystal lcd(12, 10, 11, 4, 5, 2, 3);

void setup() {
  // initialize comm-port
  Serial.begin(9600);
  
  // initialize pin mode
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(12, OUTPUT);
  pinMode(13, OUTPUT);
  
  // backlite
  pinMode(9, INPUT);
  // relay
  pinMode(6, OUTPUT);
  
  digitalWrite(2, LOW);
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  digitalWrite(5, LOW);
  digitalWrite(10, LOW);
  digitalWrite(11, LOW);
  digitalWrite(12, LOW);
  
  // turn on backlite
  lcd.begin(20,2);
  
  turnDisplayOn();
  turnPumpOn();
  
  Serial.println("Setup done");
}

void loop() { 
  
  // Simple state-machine approach
  // one-byte transitions where posible, and simple terminated values where needed
  
  switch (STATE) {
    case RUNNING:
      handleRunningState();
      break;
    case CONFIG:
      handleConfigState();
      break;
    //case DATA:
    //  handleDataState();
    //  break;
    case INIT:
      handleInitState();
      break;
    default:
      // terminated state
      // power off (if possible)
      break;
  }
  
  if(STATE != LAST_STATE){
    LAST_STATE = STATE;
  }
  
  // keep track of time and update display
  if (millis() > updateTime) {
    updateDisplay();
    updateTime = millis() + logInterval;
  }
  
  // if display is on - I really need another button
  long mils = millis();
  if(displayOn) {
    // relay-control
    if(mils > ignoreInputMillis) {
      int reading = digitalRead(9);
      if ( reading != lastReading ) {
        // start timer
        bounce = mils+50;
      }
      
      if(bounce > 0 && mils > bounce){
        //toggle relay
        if(reading == LOW) {
          // comfirm press - change state
          if(isPumpOn()) {
            turnPumpOff();
            autoRestartMillis = mils + autoRestartDuration;
            Serial.println("Pump stopped manually");
          } else {
            turnPumpOn();
            Serial.println("Pump started manually");
          }
          //autoRestartMillis = millis() + autoRestartDuration;
          
          bounce = 0;
        }
      }
      
      lastReading = reading;
    }
    
    // control of backlite
    if(millis() > backliteMillis) {
      turnBacklightOff();
    }
  }
  
  if (!displayOn && digitalRead(9) == 0) {
     turnBacklightOn();
     ignoreInputMillis = millis() + ignoreTime;
  }
  
  if(isPumpOff()){
      if(millis() > autoRestartMillis) {
          turnPumpOn();
          //autoRestartMillis = millis() + autoRestartDuration;
          Serial.println("Autostarting pump;");
      }
  }
} 

boolean isPumpOn() {
  return pumpState == LOW;
}

boolean isPumpOff() {
  return pumpState == HIGH;
}

void turnPumpOn() {
  pumpState = LOW;
  digitalWrite(6, pumpState);
  pumpMillis = millis() + pumpPrimeDuration;
}

void turnPumpOff() {
  pumpState = HIGH;
  digitalWrite(6, pumpState);
}

void turnDisplayOff() {
  Serial.println("Turning off LCD");
  lcd.clear();
  turnBacklightOff();
  lcd.noDisplay();
}

void turnBacklightOff() {
  Serial.println("Turning backlight off");
  digitalWrite(13, LOW);
  displayOn = false;
}

void turnDisplayOn() {
  Serial.println("Turning on LCD");
  lcd.display();
  turnBacklightOn();
  lcd.clear();
}

void turnBacklightOn() {
  Serial.println("Turning backlight on");
  digitalWrite(13, HIGH);
  displayOn = true;
  backliteMillis = millis() + backliteDuration;
}

void updateDisplay() {
  String line0 = "> t0  yyy xxxx hh:mm";
  String line1 = "< t1  DDDW   ddddddd    ";
  
  line0 = line0.replace("t0", t0);
  line1 = line1.replace("t1", t1);
  
  //line0 = line0.replace("a0", (int)runningT0avg);
  //line1 = line1.replace("a1", (int)runningT1avg);
  
  switch (STATE) {
    case INIT:
      line0 = line0.replace("xxxx", "Init");
      break;
    case CONFIG:
      line0 = line0.replace("xxxx", "Conf");
      break;
    case RUNNING:
      line0 = line0.replace("xxxx", " Run");
      break;   
  }
  
  if(isPumpOff()) {
    line0 = line0.replace("yyy", "off");
  } else {
    line0 = line0.replace("yyy", "on ");
  }
  
  line0 = line0.replace("hh:mm", getTimeStamp());
  line1 = line1.replace("DDD", (int)dissipated);  
  line1 = line1.replace("ddddddd", (long)dissipatedHeat);
  
  lcd.setCursor(0,0);
  lcd.print(line0);
  
  lcd.setCursor(0,1);
  lcd.print(line1);
}

String getTimeStamp() {
  time_t t = now();
  
  String result =  "";
  int h = hour(t);
  if(h<10) {
    result += "0";
  }
  result += h;
        
  int m = minute(t);
  if(m<10){
    result += ":0";
  } else {
    result += ":";
  }
  result += m;
     
  return result;
}

void handleRunningState() {
  if(Serial.available() > 0) {
    byte cmd = Serial.read();
    
    switch(cmd) {
      //case 'D':
      //  STATE = DATA;
      //  break;
      case 'C':
        STATE = CONFIG;
        break;
      case 'Q':
        STATE = STOPPED;
        break;
    }
  }
  
  if(STATE == LAST_STATE) {
    // main "program"
    // log data ...
    if( millis() > logMillis ) {
      Serial.print("Running");
      //Serial.print(".");
      t0 = getTemperature(1);
      //Serial.print(".");
      t1 = getTemperature(2);
      //Serial.print(".");
      
      Serial.print(getTimeStamp());
      Serial.print("|");
      Serial.print(t0);
      Serial.print("|");
      Serial.print(t1);
      Serial.print(";");
        
      /*if(isPumpOn()) {
        Serial.print("pump running;");
      } else {
        Serial.print("pump stopped;");
      }*/
      // Serial.println(msg);
      // logMillis += logInterval;
      //}
      // ... control environment ...
      // it is assumed that t0 > t1 in normal operation
      
      //Serial.print(".");
      if(isPumpOn()) {
        int dT = t0 -t1;
        if(dT < deltaLimit && t0 < forceTemp) {
          // to small temp-difference - shut off pump
          if(millis() > pumpMillis) {
            turnPumpOff();
            autoRestartMillis = millis() + autoRestartDuration;
            Serial.println("Turning pump off;");
          }
        }
        //Serial.print(".");
        
        // update dissipated heat
        dissipated = (heatCapacity * dT * pumpQ);
        dissipatedHeat += (dissipated * logInterval)/1000;;
        //Serial.print(dissipatedHeat);
        
        //Serial.print(".");
      }
      // ... done!
      
      //measureMillis += measureInterval;
      /*if(looped) {
        // subtract old value being overwritten
        runningT0avg = runningT0avg - ((double)logT0[pointer] / LOG_SIZE );
        runningT1avg = runningT1avg - ((double)logT1[pointer] / LOG_SIZE );
      }
      
      logT0[pointer] = t0;
      logT1[pointer] = t1;
      logState[pointer] = isPumpOn();
      
      if(looped) {
        // add new value to average
        runningT0avg = runningT0avg + ((double)logT0[pointer] / LOG_SIZE );
        runningT1avg = runningT1avg + ((double)logT1[pointer] / LOG_SIZE );
      } else {
        runningT0avg = (runningT0avg * pointer) / (pointer + 1) + ((double)logT0[pointer] / (pointer + 1) );
        runningT1avg = (runningT1avg * pointer) / (pointer + 1) + ((double)logT1[pointer] / (pointer + 1) );
      }
 
       Serial.print(runningT0avg);
       Serial.print(runningT1avg);
      
      pointer = pointer + 1;
      if(pointer == LOG_SIZE) {
        pointer = 0;
        looped = true;
      }*/
      
      logMillis = millis() + logInterval;
      
      Serial.println("done");
    }
  }
}

void handleInitState() {
  if(Serial.available() > 0) {
    byte cmd = Serial.read();
    
    switch(cmd) {
      case 'R':
        STATE = RUNNING;
        break;
      case 'C':
        STATE = CONFIG;
        break;
      case 'Q':
        STATE = STOPPED;
        break;
    }
  }
}

void handleConfigState() {
  if(Serial.available() > 0) {
    byte cmd = Serial.read();
    
    switch(cmd) {
      case 'R':
        STATE = RUNNING;
        break;
    }
  
    if ( STATE == LAST_STATE) {
      
      if(cmd == 'T') {
        // configure time
        time_t pctime = 0;
        int i = 0;
        while ( i < 10 ){
          if(Serial.available() > 0 ) {
            char c = Serial.read();
            if( c >= '0' && c <= '9'){   
              pctime = (10 * pctime) + (c - '0') ; // convert digits to a number    
            }
            i ++;
          }
        }   
        setTime(pctime);   // Sync Arduino clock to the time received on the serial port
        
        Serial.print("Time: ");
        Serial.println(now());
      }
    }
  }
}

/*void handleDataState() {
  if(Serial.available() > 0) {
    byte cmd = Serial.read();
    
    switch(cmd) {
      case 'R':
        STATE = RUNNING;
        break;
      case 'C':
        STATE = CONFIG;
        break;
    }
  
    if ( STATE == LAST_STATE) {
      
      if(cmd == 'H') {
        // dump data from last hour
        int dumpIndex = (logIndex-60) % LOG_SIZE;
        while(dumpIndex != logIndex) {
          Serial.print(pgm_read_byte_near(&logT0[dumpIndex]), BYTE);
          Serial.print(pgm_read_byte_near(&logT1[dumpIndex]), BYTE);
          dumpIndex = (dumpIndex + 1) % LOG_SIZE;
        }
      } else if(cmd == 'D') {
        // dump data from last 24 hours
        int dumpIndex = (logIndex-1440) % LOG_SIZE;
        while(dumpIndex != logIndex) {
          Serial.print(pgm_read_byte_near(&logT0[dumpIndex]), BYTE);
          Serial.print(pgm_read_byte_near(&logT1[dumpIndex]), BYTE);
          dumpIndex = (dumpIndex + 1) % LOG_SIZE;
        }
      }
    }
  }
}*/

//void connect() {
//  while (Serial.available() <= 0) {
//    Serial.print(START_TRANSMISSION, BYTE);
//  }
//}

int getTemperature(int pin) {
  byte samples = 8;
  
  // gets 9 samples of temperature
  // the first is disgarded, the rest averaged
  int tempc = analogRead(pin);
  tempc = 0;
  for(byte i = 0; i<samples; i++){
    tempc += analogRead(pin);
  }
  
  tempc = (5.0 * tempc * 100.0) / (samples * 1024.0);
  
  return tempc;
}
